﻿//////////////////////////////////////////////////////////////////
//
// Almanac\Location.cs
//
// Copyright (c) 2012 Dan Pike.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation files
// (the "Software"), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software,
// and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
using Factory;
using Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Utilities;
using System.Text.RegularExpressions;
using System.ComponentModel;

namespace Almanac
{
   public class Location : ILocation
   {
      private static Logger logger_;
      private static Regex regex_;

      private float altitude_;
      private Hemisphere longitudeHemisphere_;
      private int longitudeDegrees_;
      private int longitudeMinutes_;
      private float longitudeSeconds_;
      private Hemisphere latitudeHemisphere_;
      private int latitudeDegrees_;
      private int latitudeMinutes_;
      private float latitudeSeconds_;
      private string asString_;

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      static Location()
      {
         logger_ = Program.Logger;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      public Location()
      {
         longitudeDegrees_ = longitudeMinutes_ = latitudeDegrees_ = latitudeMinutes_ = 0;
         altitude_ = longitudeSeconds_ = latitudeSeconds_ = 0.0f;
         longitudeHemisphere_ = latitudeHemisphere_ = Hemisphere.None;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of Object
      /// </summary>
      public override bool Equals(object otherObject)
      {
         ILocation other = otherObject as ILocation;
         return (null != other)
            && (other.Longitude == this.Longitude)
            && (other.Latitude == this.Latitude)
            && (other.Altitude == this.Altitude)
            ;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of Object
      /// </summary>
      public override int GetHashCode()
      {
         // We know that the location is most likely to stay within the range
         // N24-N49 & W57-W126, so we can cram them into an int
         int code = (Hemisphere.North == LatitudeHemisphere) ? 0 : 1;
         unchecked
         {
            code =  2 * code + LatitudeDegrees - 24;
            code = 60 * code + LatitudeMinutes;
            code = 60 * code + (int)LatitudeSeconds;
            code = 60 * code + ((Hemisphere.East == LongitudeHemisphere) ? 0 : 1);
            code =  2 * code + (LongitudeDegrees - 67);
            code = 60 * code + LongitudeMinutes;
            code = 60 * code + (int)LongitudeSeconds;
         }
         return code;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public ILocation FromString(string format)
      {
         try
         {
            if (null == regex_)
            {
               // Match pattern like: "[N.49.14.44.2203,W122.7.13,-13.5]
               // Note the overloading of period '.' which may separate degrees from minutes
               // etc but, for the seconds and altitude, is a standard decimal
               regex_ = new Regex(
                    @"^\s*\[\s*"
                  + @"(?<latHem>(?:N(?:orth)?)|(?:S(?:outh)?))\s*\.\s*"
                  + @"(?<latDeg>(?:\+|\-)?\d+)\s*\.\s*"
                  + @"(?<latMin>\d+)\s*\.\s*"
                  + @"(?<latSec>\d+(?:.\d*)?)\s*"
                  + @",\s*"
                  + @"(?<longHem>(?:E(?:ast)?)|(?:W(?:est)?))\s*\.\s*"
                  + @"(?<longDeg>(?:\+|\-)?\d+)\s*\.\s*"
                  + @"(?<longMin>\d+)\s*\.\s*"
                  + @"(?<longSec>\d+(?:.\d*)?)\s*"
                  + @"(?:,\s*(?<alt>(?:\+|\-)?\d+(?:.\d*)?))?\s*"
                  + @"\]\s*$",
                  RegexOptions.Compiled | RegexOptions.IgnoreCase);
            }
            Match match = regex_.Match(format.ToUpper());
            if (match.Success)
            {
               LatitudeHemisphere = match.Groups["latHem"].Value.StartsWith("N", StringComparison.CurrentCultureIgnoreCase) ? Hemisphere.North : Hemisphere.South;
               LatitudeDegrees = int.Parse(match.Groups["latDeg"].Value);
               LatitudeMinutes = int.Parse(match.Groups["latMin"].Value);
               LatitudeSeconds = float.Parse(match.Groups["latSec"].Value);

               LongitudeHemisphere = match.Groups["longHem"].Value.StartsWith("E", StringComparison.CurrentCultureIgnoreCase) ? Hemisphere.East : Hemisphere.West;
               LongitudeDegrees = int.Parse(match.Groups["longDeg"].Value);
               LongitudeMinutes = int.Parse(match.Groups["longMin"].Value);
               LongitudeSeconds = float.Parse(match.Groups["longSec"].Value);

               Group altitude = match.Groups["alt"];
               altitude_ = altitude.Success ? float.Parse(altitude.Value) : 0.0f;
               return this;
            }
            else
            {
               logger_.ErrorFormat("Invalid location string '{0}'", format);
            }
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         return null;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public ILocation Clone()
      {
         // Saves typing to have a custom function for this common operation!
         return Program.Loader.Create<ILocation>().CopyFrom(this);
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public ILocation Move(float latitudeOffset, float longitudeOffset)
      {
         return Move(latitudeOffset, longitudeOffset, 0.0f);
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public ILocation Move(float latitudeOffset, float longitudeOffset,
         float altitudeChange)
      {
         // Keep longitude in the range -180 < value <= +180
         float newLongitude = (Longitude + longitudeOffset) % (360.0f * 3600.0f);
         if ((180.0f * 3600.0f) < newLongitude)
         {
            newLongitude -= (360.0f * 3600.0f);
         }
         else if (-(180.0f * 3600.0f) >= newLongitude)
         {
            newLongitude += (360.0f * 3600.0f);
         }
         Longitude = newLongitude;

         // Keep latitude in the range -90 <= latitude <= +90. We check for
         // the pole-singularity at +90 later
         float newLatitude = (Latitude + latitudeOffset) % (360.0f * 3600.0f);
         if ((270.0f * 3600.0f) <= newLatitude)
         {
            newLatitude -= (360.0f * 3600.0f);
         }
         else if ((90.0f * 3600.0f) < newLatitude)
         {
            newLatitude = (180.0f * 3600.0f) - newLatitude;
         }
         else if (-(270.0f * 3600.0f) >= newLatitude)
         {
            newLatitude += (360.0f * 3600.0f);
         }
         else if (-(90.0f * 3600.0f) > newLatitude)
         {
            newLatitude = -((180.0f * 3600.0f) + newLatitude);
         }
         // Will throw an exception iff newLatitude is exactly +/- 90.0 degrees
         Latitude = newLatitude;

         // Go up or down
         Altitude += altitudeChange;
         return this;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public ILocation CopyFrom(ILocation src)
      {
         longitudeHemisphere_ = src.LongitudeHemisphere;
         longitudeDegrees_ = src.LongitudeDegrees;
         longitudeMinutes_ = src.LongitudeMinutes;
         longitudeSeconds_ = src.LongitudeSeconds;
         latitudeHemisphere_ = src.LatitudeHemisphere;
         latitudeDegrees_ = src.LatitudeDegrees;
         latitudeMinutes_ = src.LatitudeMinutes;
         latitudeSeconds_ = src.LatitudeSeconds;
         altitude_ = src.Altitude;
         asString_ = null;
         return this;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public float Longitude
      {
         get
         {
            float absResult = longitudeDegrees_ * 3600.0f + longitudeMinutes_ * 60.0f + longitudeSeconds_;
            return (Hemisphere.East == LongitudeHemisphere) ? absResult : -absResult;
         }
         set
         {
            if ((-180.0f * 3600.0f >= value) || (180.0f * 3600.0f < value))
            {
               // Try using the move() method instead
               throw new ArgumentOutOfRangeException("longitude", value, "too many arc-seconds");
            }
            longitudeHemisphere_ = (0 <= value) ? Hemisphere.East : Hemisphere.West;
            float absValue = Math.Abs(value);
            longitudeDegrees_ = (int)Math.Abs(absValue / 3600.0f);
            float remainder =  absValue % 3600.0f;
            longitudeMinutes_ = (int)Math.Abs(remainder / 60.0f);
            longitudeSeconds_ = remainder % 60.0f;

            asString_ = null;
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public Hemisphere LongitudeHemisphere
      {
         get { return longitudeHemisphere_; }
         set
         {
            if ((Hemisphere.East == value) || (Hemisphere.West == value))
            {
               longitudeHemisphere_ = value;
            }
            else
            {
               throw new InvalidOperationException(
                  string.Format("cannot set longitudeHemisphere to '{0}'", value));
            }
            asString_ = null;
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public int LongitudeDegrees
      {
         get { return longitudeDegrees_; }
         set
         {
            longitudeDegrees_ = value;
            asString_ = null;
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public int LongitudeMinutes
      {
         get { return longitudeMinutes_; }
         set 
         {
            longitudeMinutes_ = value;
            asString_ = null;
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public int LongitudeTotalMinutes
      {
         get
         {
            int sign = (Hemisphere.East == longitudeHemisphere_) ? 1 : -1;
            return sign * (longitudeDegrees_ * 60 + longitudeMinutes_);
         }
         set
         {
            if ((-180.0f * 3600.0f >= value) || (180.0f * 3600.0f < value))
            {
               // Try using the move() method instead
               throw new ArgumentOutOfRangeException("longitudeTotalMinutes", value, "too many arc-seconds");
            }
            longitudeDegrees_ = Math.Abs(value / 60);
            longitudeMinutes_ = Math.Abs(value % 60);
            longitudeHemisphere_ = (0 <= value) ? Hemisphere.East : Hemisphere.West;
            longitudeSeconds_ = 0;
            asString_ = null;
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public float LongitudeSeconds
      {
         get { return longitudeSeconds_; }
         set 
         {
            longitudeSeconds_ = value;
            asString_ = null;
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public float Latitude
      {
         get
         {
            float absResult = latitudeDegrees_ * 3600.0f + latitudeMinutes_ * 60.0f + latitudeSeconds_;
            return (Hemisphere.North == LatitudeHemisphere) ? absResult : -absResult;
         }
         set 
         {
            if ((-90.0f * 3600.0f >= value) || (90.0f * 3600.0f <= value))
            {
               // Try using the move() method instead
               throw new ArgumentOutOfRangeException("latitude", value, "too many arc-seconds");
            }
            latitudeHemisphere_ = (0 <= value) ? Hemisphere.North : Hemisphere.South;
            float absValue = Math.Abs(value);
            latitudeDegrees_ = (int)Math.Abs(absValue / 3600.0f);
            float remainder = absValue % 3600.0f;
            latitudeMinutes_ = (int)Math.Abs(remainder / 60.0f);
            latitudeSeconds_ = remainder % 60.0f;

            asString_ = null;
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public Hemisphere LatitudeHemisphere
      {
         get { return latitudeHemisphere_; }
         set
         {
            if ((Hemisphere.North == value) || (Hemisphere.South == value))
            {
               latitudeHemisphere_ = value;
            }
            else
            {
               throw new InvalidOperationException(
                  string.Format("cannot set latitudeHemisphere to '{0}'", value));
            }
            asString_ = null;
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public int LatitudeDegrees
      {
         get { return latitudeDegrees_; }
         set
         {
            latitudeDegrees_ = value;
            asString_ = null;
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public int LatitudeMinutes
      {
         get { return latitudeMinutes_; }
         set
         {
            latitudeMinutes_ = value;
            asString_ = null;
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public int LatitudeTotalMinutes
      {
         get
         {
            int sign = (Hemisphere.North == latitudeHemisphere_) ? 1 : -1;
            return sign * (latitudeDegrees_ * 60 + latitudeMinutes_);
         }
         set
         {
            if ((-90.0f * 60.0f >= value) || (90.0f * 60.0f <= value))
            {
               // Try using the move() method instead
               throw new ArgumentOutOfRangeException("latitudeTotalMinutes", value, "too many arc-seconds");
            }
            latitudeDegrees_ = Math.Abs(value / 60);
            latitudeMinutes_ = Math.Abs(value % 60);
            latitudeHemisphere_ = (0 <= value) ? Hemisphere.North : Hemisphere.South;
            longitudeSeconds_ = 0;
            asString_ = null;
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public float LatitudeSeconds
      {
         get { return latitudeSeconds_; }
         set
         {
            latitudeSeconds_ = value;
            asString_ = null;
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILocation
      /// </summary>
      public float Altitude
      {
         get { return altitude_; }
         set { altitude_ = value; asString_ = null; }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      public override string ToString()
      {
         if (null == asString_)
         {
            StringBuilder sb = new StringBuilder();
            sb.Append('[');
            sb.Append(LatitudeHemisphere);
            sb.Append('.');
            sb.Append(LatitudeDegrees);
            sb.Append('.');
            sb.Append(LatitudeMinutes);
            sb.Append('.');
            sb.Append(LatitudeSeconds);
            sb.Append(',');
            sb.Append(LongitudeHemisphere);
            sb.Append('.');
            sb.Append(LongitudeDegrees);
            sb.Append('.');
            sb.Append(LongitudeMinutes);
            sb.Append('.');
            sb.Append(LongitudeSeconds);
            if (0.0f != Altitude)
            {
               sb.Append(',');
               sb.Append(Altitude);
            }
            sb.Append(']');
            asString_ = sb.ToString();
         }
         return asString_;
      }
   }
}
